package web

import (
	lru "github.com/hashicorp/golang-lru"
	"io"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
)

type FileUploader struct {
	FileField string
	// 返回文件保存路径
	// 如果框架来设名称要考虑重名问题(可以使用UUID解决)
	DataPathFunc func(file *multipart.FileHeader) string
}

func (u FileUploader) Handle() HandleFunc {
	if u.FileField == "" {
		u.FileField = "file"
	}
	if u.DataPathFunc == nil {
		// 设置默认值
		u.DataPathFunc = func(file *multipart.FileHeader) string {
			return filepath.Join("testdata", "upload", file.Filename)
		}
	}
	return func(ctx *Context) {
		// 上传文件的逻辑
		// 1. 读取文件内容
		/*
			// A FileHeader describes a file part of a multipart request.
			type FileHeader struct {
				Filename string
				Header   textproto.MIMEHeader
				Size     int64

				content   []byte
				tmpfile   string
				tmpoff    int64
				tmpshared bool
			}
		*/
		file, fileHeader, err := ctx.Req.FormFile(u.FileField)
		if err != nil {
			ctx.RespStatusCode = 500
			ctx.RespData = []byte("上传失败 " + err.Error())
			return
		}
		defer file.Close()
		// 2. 计算目标路径
		// 将目标路径计算的逻辑交给用户
		dst := u.DataPathFunc(fileHeader)
		// 可以尝试把dst里不存在的目录都创建,防止报错
		index := strings.LastIndex(dst, "\\")
		err = os.MkdirAll(dst[:index], os.ModePerm)
		if err != nil {
			ctx.RespStatusCode = http.StatusInternalServerError
			ctx.RespData = []byte("上传失败 " + err.Error())
			return
		}

		// os.O_WRONLY 可写
		// os.O_TRUNC 存在就清空
		// os.O_CREATE 不存在就创建
		dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o666)
		if err != nil {
			ctx.RespStatusCode = http.StatusInternalServerError
			ctx.RespData = []byte("上传失败 " + err.Error())
			return
		}
		defer dstFile.Close()
		// 3. 保存文件
		// buf(参数3)指定每次传输多大一段, 会影响性能
		// 要考虑复用, 如果传nil, 有默认提供
		_, err = io.CopyBuffer(dstFile, file, nil)
		if err != nil {
			ctx.RespStatusCode = http.StatusInternalServerError
			ctx.RespData = []byte("上传失败 " + err.Error())
			return
		}
		// 4. 返回响应
		ctx.RespStatusCode = http.StatusOK
		ctx.RespData = []byte("上传成功")
	}
}

type FileDownloader struct {
	Dir string
}

func (d FileDownloader) Handle() HandleFunc {
	return func(ctx *Context) {
		// xxx?file=xxx
		filename, err := ctx.QueryValue("file")
		if err != nil {
			ctx.RespStatusCode = http.StatusBadRequest
			ctx.RespData = []byte("找不到目标文件")
			return
		}
		// 安全性提升(一点点)
		filename = filepath.Clean(filename)
		dst := filepath.Join(d.Dir, filename)
		// 转绝对路径(相对路径可能被拿到私密文件)
		//dst,err=filepath.Abs(dst)
		//if strings.Contains(d.Dir,filename){
		//
		//}
		fn := filepath.Base(dst)

		header := ctx.Resp.Header()
		header.Set("Content-Disposition", "attachment;filename="+fn)
		header.Set("Content-Description", "File Transfer")
		// 同样二进制文件
		header.Set("Content-Type", "application/octet-stream")
		header.Set("Content-Transfer-Encoding", "binary")
		// 不会缓存
		header.Set("Expires", "0")
		header.Set("Cache-Control", "must-revalidate")
		header.Set("Pragma", "public")

		http.ServeFile(ctx.Resp, ctx.Req, dst)
	}
}

type StaticResourceHandlerOption func(handler *StaticResourceHandler)

type StaticResourceHandler struct {
	dir string
	// 设置type防止有些浏览器无法解析文件
	// 根据文件后缀名匹配type
	extensionContextTypeMap map[string]string
	cache                   *lru.Cache // 缓存
	maxSize                 int        // 大文件不缓存
}

func NewStaticResourceHandler(dir string, opts ...StaticResourceHandlerOption) (*StaticResourceHandler, error) {
	// New(总共缓存多少个k-y)
	cache, err := lru.New(1000)
	if err != nil {
		return nil, err
	}
	res := &StaticResourceHandler{
		dir:     dir,
		cache:   cache,
		maxSize: 1024 * 1024 * 10, // 10MB
		extensionContextTypeMap: map[string]string{
			"jpeg": "image/jpeg",
			"jpg":  "image/jpg",
			"jpe":  "image/jpeg",
			"png":  "image/png",
			"pdf":  "image/pdf",
		},
	}
	for _, opt := range opts {
		opt(res)
	}
	return res, nil
}

func StaticWithMaxFileSize(maxSize int) StaticResourceHandlerOption {
	return func(handler *StaticResourceHandler) {
		handler.maxSize = maxSize
	}
}

func StaticWithCache(c *lru.Cache) StaticResourceHandlerOption {
	return func(handler *StaticResourceHandler) {
		handler.cache = c
	}
}

func StaticWithMoreExtension(extMap map[string]string) StaticResourceHandlerOption {
	return func(h *StaticResourceHandler) {
		for ext, contenType := range extMap {
			h.extensionContextTypeMap[ext] = contenType
		}
	}
}

func (s *StaticResourceHandler) Handle(ctx *Context) {
	// 1. 拿到目标文件名
	file, err := ctx.PathValue("file")
	if err != nil {
		ctx.RespStatusCode = http.StatusBadRequest
		ctx.RespData = []byte("请求路径错误")
		return
	}

	dst := filepath.Join(s.dir, file)
	// jpg/txt/video/audio ...
	ext := filepath.Ext(dst)[1:]
	header := ctx.Resp.Header()

	if data, ok := s.cache.Get(dst); ok {
		header.Set("Content-Type", s.extensionContextTypeMap[ext])
		header.Set("Content-Length", strconv.Itoa(len(data.([]byte))))
		header.Set("Web-Msg", "from cache")
		ctx.RespData = data.([]byte)
		ctx.RespStatusCode = 200
		return
	}

	// 2. 定位到目标文件,读取
	//dst := filepath.Join(s.dir, file)
	data, err := ioutil.ReadFile(dst)
	if err != nil {
		ctx.RespStatusCode = http.StatusInternalServerError
		ctx.RespData = []byte("服务器系统错误")
		return
	}

	// 缓存
	if len(data) <= s.maxSize {
		s.cache.Add(dst, data)
	}

	// 3. 返回
	header.Set("Content-Type", s.extensionContextTypeMap[ext])
	header.Set("Content-Length", strconv.Itoa(len(data)))
	ctx.RespData = data
	ctx.RespStatusCode = 200
}
